#ifndef DATETIMEFORMAT_HPP
#define DATETIMEFORMAT_HPP

#include <string>
#include <memory>
#include <ctime>

namespace base
{
namespace utils
{

const std::string MONTH_WORD[12] = {"January", "February", "March", "April", "May", "June",
                                    "July", "August", "September", "October", "November", "December"};

const std::string WEEK_WORD[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

/**
 * 日期格式化，根据Qt源码改写而来，和Qt的格式化表达式相同
 *
 * These expressions may be used for the date:
 * d    :   the day as number without a leading zero (1 to 31)
 * dd   :   the day as number with a leading zero (01 to 31)
 * ddd  :   the abbreviated localized day name (e.g. 'Mon' to 'Sun'). Uses the system locale to localize the name, i.e. QLocale::system().
 * dddd :   the long localized day name (e.g. 'Monday' to 'Sunday'). Uses the system locale to localize the name, i.e. QLocale::system().
 * M    :   the month as number without a leading zero (1-12)
 * MM   :   the month as number with a leading zero (01-12)
 * MMM  :   the abbreviated localized month name (e.g. 'Jan' to 'Dec'). Uses the system locale to localize the name, i.e. QLocale::system().
 * MMMM :   the long localized month name (e.g. 'January' to 'December'). Uses the system locale to localize the name, i.e. QLocale::system().
 * yy   :   the year as two digit number (00-99)
 * yyyy :   the year as four digit number
 * h    :   the hour without a leading zero (0 to 23 or 1 to 12 if AM/PM display)
 * hh   :   the hour with a leading zero (00 to 23 or 01 to 12 if AM/PM display)
 * H    :   the hour without a leading zero (0 to 23, even with AM/PM display)
 * HH   :   the hour with a leading zero (00 to 23, even with AM/PM display)
 * m    :   the minute without a leading zero (0 to 59)
 * mm   :   the minute with a leading zero (00 to 59)
 * s    :   the second without a leading zero (0 to 59)
 * ss   :   the second with a leading zero (00 to 59)
 * z    :   the milliseconds without leading zeroes (0 to 999)
 * zzz  :   the milliseconds with leading zeroes (000 to 999)
 * AP/A :   use AM/PM display. A/AP will be replaced by either "AM" or "PM".
 * ap/a :   use am/pm display. a/ap will be replaced by either "am" or "pm".
 *
 * @author: sherlock_lht
 */
class DateTimeFormat
{
public:
  /**
   * time_t按照指定format格式化，这里也可以直接传入时间戳，1900距今的秒数
   * @author: sherlock_lht
   */
  static std::string formatDateTime(const std::string &format, time_t time, long long int msecond)
  {
    tm *tm = localtime(&time);
    return formatDateTime(format, tm, msecond);
  }
  /**
   * tm按照指定的format格式化
   * @author: sherlock_lht
   */
  static std::string formatDateTime(const std::string &format, tm *tm, long long int msecond)
  {
    if (nullptr == tm)
    {
      return std::string("");
    }
    std::string result;
    int i = 0;
    while (i < format.size())
    {
      if (format.at(i) == '\'')
      {
        result.append(readEscapedFormatString(format, &i));
        continue;
      }
      const char c = format.at(i);
      int repeat = repeatCount(format, i);
      bool used = false;
      switch (c)
      {
        case 'y':
          used = true;
          if (repeat >= 4)
            repeat = 4;
          else if (repeat >= 2)
            repeat = 2;
          switch (repeat)
          {
            case 4:
              result.append(formatStr("%04d", tm->tm_year + 1900));
              break;
            case 2:
              result.append(formatStr("%02d", (tm->tm_year + 1900) % 100));
              break;
            default:
              repeat = 1;
              result.push_back(c);
              break;
          }
          break;
        case 'M':
          used = true;
          repeat = std::min(repeat, 4);
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%01d", tm->tm_mon + 1));
              break;
            case 2:
              result.append(formatStr("%02d", tm->tm_mon + 1));
              break;
            case 3:
              result.append(getMonthWord(tm->tm_mon, 3));
              break;
            case 4:
              result.append(getMonthWord(tm->tm_mon));
              break;
          }
          break;
        case 'd':
          used = true;
          repeat = std::min(repeat, 4);
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%01d", tm->tm_mday));
              break;
            case 2:
              result.append(formatStr("%02d", tm->tm_mday));
              break;
            case 3:
              result.append(getWeekWork(tm->tm_wday, 3));
              break;
            case 4:
              result.append(getWeekWork(tm->tm_wday));
              break;
          }
          break;
        case 'h':
        {
          used = true;
          repeat = std::min(repeat, 2);
          int hour = tm->tm_hour;
          if (timeFormatContainsAP(format))
          {
            if (hour > 12)
              hour -= 12;
            else if (hour == 0)
              hour = 12;
          }
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%d", hour));
              break;
            case 2:
              result.append(formatStr("%02d", hour));
              break;
          }
          break;
        }
        case 'H':
          used = true;
          repeat = std::min(repeat, 2);
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%d", tm->tm_hour));
              break;
            case 2:
              result.append(formatStr("%02d", tm->tm_hour));
              break;
          }
          break;
        case 'm':
          used = true;
          repeat = std::min(repeat, 2);
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%d", tm->tm_min));
              break;
            case 2:
              result.append(formatStr("%02d", tm->tm_min));
              break;
          }
          break;
        case 's':
          used = true;
          repeat = std::min(repeat, 2);
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%d", tm->tm_sec));
              break;
            case 2:
              result.append(formatStr("%02d", tm->tm_sec));
              break;
          }
          break;
        case 'a':
          used = true;
          if (i + 1 < format.length() && format.at(i + 1) == 'p')
          {
            repeat = 2;
          }
          else
          {
            repeat = 1;
          }
          result.append(tm->tm_hour < 12 ? "am" : "pm");
          break;
        case 'A':
          used = true;
          if (i + 1 < format.length() && format.at(i + 1) == 'P')
          {
            repeat = 2;
          }
          else
          {
            repeat = 1;
          }
          result.append(tm->tm_hour < 12 ? "AM" : "PM");
          break;
        case 'z':
          used = true;
          if (repeat >= 3)
          {
            repeat = 3;
          }
          else
          {
            repeat = 1;
          }
          switch (repeat)
          {
            case 1:
              result.append(formatStr("%d", msecond));
              break;
            case 3:
              result.append(formatStr("%03d", msecond));
              break;
          }
          break;
        default:
          break;

      }
      if (!used)
      {
        result.append(std::string(repeat, c));
      }
      i += repeat;
    }

    return result;
  }
private:
  static std::string getMonthWord(int tm_month, int sep_length = -1)
  {
    if (tm_month < 0 || tm_month > 11)
    {
      return std::string("");
    }
    return MONTH_WORD[tm_month].substr(0, sep_length);
  }

  static std::string getWeekWork(int tm_week, int sep_length = -1)
  {
    if (tm_week < 0 || tm_week > 6)
    {
      return std::string("");
    }
    return WEEK_WORD[tm_week].substr(0, sep_length);
  }

  static std::string readEscapedFormatString(const std::string &format, int *idx)
  {
    int &i = *idx;
    ++i;
    if (i == format.size())
      return std::string("");
    if (format.at(i) == '\'')// "''" outside of a quoted stirng
    {
      ++i;
      return std::string("'");
    }

    std::string result;
    while (i < format.size())
    {
      if (format.at(i) == '\'')
      {
        if (i + 1 < format.size() && format.at(i + 1) == '\'')
        {
          result.push_back('\'');
          i += 2;
        }
        else
        {
          break;
        }
      }
      else
      {
        result.push_back(format.at(i++));
      }
    }
    if (i < format.size())
    {
      ++i;
    }
    return result;
  }

  static bool timeFormatContainsAP(const std::string &format)
  {
    int i = 0;
    while (i < format.size())
    {
      if (format.at(i) == '\'')
      {
        readEscapedFormatString(format, &i);
        continue;
      }
      if (std::tolower(format.at(i)) == 'a')
      {
        return true;
      }
      ++i;
    }
    return false;
  }

  static int repeatCount(const std::string &s, int i)
  {
    char c = s.at(i);
    int j = i + 1;
    while (j < s.size() && s.at(j) == c)
      ++j;
    return j - i;
  }

  template<typename ... Args>
  static std::string formatStr(const char *format, Args ... args)
  {
    auto size_buf = std::snprintf(nullptr, 0, format, args ...) + 1;
    std::unique_ptr<char[]> buf(new(std::nothrow) char[size_buf]);

    if (!buf)
      return std::string("");

    std::snprintf(buf.get(), size_buf, format, args ...);
    return std::string(buf.get(), buf.get() + size_buf - 1);
  }

};
}
}

#endif //DATETIMEFORMAT_HPP
